package com.vteba.security.provider;

import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.crypto.password.StandardPasswordEncoder;
import org.springframework.util.Assert;

import com.vteba.common.exception.BasicException;
import com.vteba.security.filter.DefaultUserAuthenticationToken;
import com.vteba.security.spi.UserDetailsService;
import com.vteba.security.spi.cache.EmptySecurityUserCache;
import com.vteba.security.spi.cache.SecurityUserCache;
import com.vteba.security.user.IUserDetails;

/**
 * 认证提供者。结合缓存和UserDetailService（数据库）。
 * @author yinlei
 * @date 2014年11月4日 上午10:51:34
 */
public class DefaultAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
	private PasswordEncoder passwordEncoder = new StandardPasswordEncoder();

	private UserDetailsService userDetailsService;

	private SecurityUserCache securityUserCache;
	
	public DefaultAuthenticationProvider() {
		
	}
	
	public Authentication authenticate(Authentication authentication)
			throws AuthenticationException {
		String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName();
		
		String tenantId = null;
		DefaultUserAuthenticationToken token = null;
		if (authentication instanceof DefaultUserAuthenticationToken) {
			token = (DefaultUserAuthenticationToken) authentication;
		} else {
			throw new BasicException("Authentication must be DefaultUserAuthenticationToken's instance.");
		}
		tenantId = token.getOrgCode();
		
		boolean cacheWasUsed = true;
		IUserDetails user = securityUserCache.getUserFromCache(username, tenantId);

		if (user == null) {
			cacheWasUsed = false;

			try {
				user = loadUser(username, token);
			} catch (UsernameNotFoundException notFound) {
				logger.debug("User '" + username + "' not found");

				if (hideUserNotFoundExceptions) {
					throw new BadCredentialsException(messages.getMessage(
							"AbstractUserDetailsAuthenticationProvider.badCredentials",
							"Bad credentials"));
				} else {
					throw notFound;
				}
			}
			Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
		}

		try {
			getPreAuthenticationChecks().check(user);
			additionalAuthenticationChecks(user, (DefaultUserAuthenticationToken) authentication);
		} catch (AuthenticationException exception) {
			if (cacheWasUsed) {
				// There was a problem, so try again after checking
				// we're using latest data (i.e. not from the cache)
				cacheWasUsed = false;
				user = loadUser(username, token);
				getPreAuthenticationChecks().check(user);
				additionalAuthenticationChecks(user, (DefaultUserAuthenticationToken) authentication);
			} else {
				throw exception;
			}
		}

		getPostAuthenticationChecks().check(user);

		if (!cacheWasUsed) {
			securityUserCache.putUserInCache(user);
		}

		Object principalToReturn = user;

		return createSuccessAuthentication(principalToReturn, authentication, user);
	}
	
	protected void additionalAuthenticationChecks(IUserDetails userDetails,
			AbstractAuthenticationToken authentication) throws AuthenticationException {
		if (authentication.getCredentials() == null) {
			logger.debug("Authentication failed: no credentials provided");
			throw new BadCredentialsException(messages.getMessage(
					"AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
		}

		String presentedPassword = authentication.getCredentials().toString();
		if (presentedPassword.equals(userDetails.getPassword())) {
			logger.debug("Authentication success: load user from cache.");
		} else {
			if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
				logger.debug("Authentication failed: password does not match stored value");
				
				throw new BadCredentialsException(messages.getMessage(
						"AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
			}
		}
	}

	protected Authentication createSuccessAuthentication(Object principal,
			Authentication authentication, IUserDetails user) {
		// Ensure we return the original credentials the user supplied,
		// so subsequent attempts are successful even with encoded passwords.
		// Also ensure we return the original getDetails(), so that future
		// authentication events after cache expiry contain the details
		// 使用我们自己的Token
		DefaultUserAuthenticationToken result = new DefaultUserAuthenticationToken(
				principal, authentication.getCredentials(),
				user.getAuthorities());
		result.setOrgCode(user.getTenantCode());
		result.setDetails(authentication.getDetails());

		return result;
	}
	
	protected void doAfterPropertiesSet() throws Exception {
		Assert.notNull(this.userDetailsService, "A UserDetailsService must be set");
		if (securityUserCache == null) {
			if (getUserCache() instanceof SecurityUserCache) {
				securityUserCache = (SecurityUserCache) getUserCache();
			} else {
				// 空实现
				securityUserCache = new EmptySecurityUserCache();
			}
		}
	}

	protected final UserDetails retrieveUser(String username,
			UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
		DefaultUserAuthenticationToken token = new DefaultUserAuthenticationToken(authentication.getPrincipal(), authentication.getCredentials());
		IUserDetails loadedUser = loadUser(username, token);
		return loadedUser;
	}

	protected IUserDetails loadUser(String username,
			DefaultUserAuthenticationToken authentication) throws AuthenticationException {
		IUserDetails loadedUser;

		try {
			loadedUser = userDetailsService.loadUser(username, authentication);
		} catch (UsernameNotFoundException notFound) {
			throw notFound;
		} catch (Exception repositoryProblem) {
			throw new InternalAuthenticationServiceException(repositoryProblem.getMessage(),
					repositoryProblem);
		}

		if (loadedUser == null) {
			throw new InternalAuthenticationServiceException(
					"UserDetailsService returned null, which is an interface contract violation");
		}
		return loadedUser;
	}
	
	public boolean supports(Class<?> authentication) {
		return (DefaultUserAuthenticationToken.class.isAssignableFrom(authentication));
	}
	
	public void setUserDetailsService(UserDetailsService userDetailsService) {
		this.userDetailsService = userDetailsService;
	}

	protected UserDetailsService getUserDetailsService() {
		return userDetailsService;
	}

	protected PasswordEncoder getPasswordEncoder() {
		return passwordEncoder;
	}

	public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
		this.passwordEncoder = passwordEncoder;
	}

	public SecurityUserCache getSecurityUserCache() {
		return securityUserCache;
	}

	public void setSecurityUserCache(SecurityUserCache securityUserCache) {
		this.securityUserCache = securityUserCache;
	}

	@Override
	protected void additionalAuthenticationChecks(UserDetails userDetails,
			UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
		logger.info("invoke old additionalAuthenticationChecks(UsernamePasswordAuthenticationToken)");
	}
}
